Continuous Integration сервер на Эрланг за 30 минут В рамкам серии образовательных статей о том как написать свой PaaS в 500 строчек, задля популяризации Эрланга, продвижение оупен соурс, а также движения "Пиши Мало думай Хорошо" сегодня мы покажем как написать свой CI сервер на Эрланг с помощью Cowboy и N2O. Мы будем использовать такие приложения: {deps, [ {cowboy, ".*", {git, "git://github.com/extend/cowboy.git", "master"}}, {sync, ".*", {git, "git://github.com/doxtop/sync", "HEAD"}}, {erlydtl, ".*", {git, "git://github.com/voxoz/erlydtl.git", "HEAD"}}, {n2o, ".*", {git, "git://github.com/5HT/n2o", "HEAD"}}, {lager, ".*", {git, "git://github.com/basho/lager", {tag,"1.2.2"}}} ]}. N2O нам нужен для того, что бы быстро накидать сайтик и REST интерфейс к нашему серверу. Вообще-то это не просто CI, это агент LXC контейнера в нашем Erlang PaaS, который будет уметь конфигурировать релизы, добавлять и удалять приложения, деплоить это все в Xen и т.д. Но это все потом, а сейчас напишем хуки для Ковбоя: Dispatch = cowboy_router:compile( [{'_', [ {"/static/[...]", cowboy_static, [{directory, {priv_dir, releaseman, [<<"static">>]}}, {mimetypes, {fun mimetypes:path_to_mimes/2, default}}]}, {"/rest/:bucket", n2o_rest, []}, %% for releases REST interface {"/rest/:bucket/:key", n2o_rest, []}, {"/rest/:bucket/:key/[...]", n2o_rest, []}, {"/github/hook/[...]", github_handler, []}, {'_', n2o_cowboy, []} ]} ]), На гитхабе прописывем Веб Хук http://synrc.com/github/hook/USER/REPO для определенного юзер репо, например для репозитория 5HT/n2o: http://synrc.com/github/hook/5HT/n2o. напишем этот обработчик: -module(github_handler). -compile(export_all). -behaviour(cowboy_http_handler). -export([init/3, handle/2, terminate/3]). handle(Req, State) -> {Params,NewReq} = cowboy_req:path(Req), Path = lists:reverse(string:tokens(binary_to_list(Params),"/")), [Repo,User|Rest] = Path, Allowed = lists:member(User,["5HT","doxtop","voxoz","synrc","spawnproc"]), ResponseReq = case Allowed of true -> case global:whereis_name("builder") of undefined -> spawn(fun() -> global:register_name("builder",self()), builder() end); Pid -> global:send("builder",{build,Repo,User}) end, HTML = wf:to_binary(["

202 Project Started to Build

", "",User,"-",Repo,""]), {ok, Req3} = cowboy_req:reply(202, [], HTML, NewReq), Req3; false -> NA = wf:to_binary(["

404 User not allowed

"]), {ok, Req4} = cowboy_req:reply(404, [], NA, NewReq), Req4 end, {ok, ResponseReq, State}. здесь важно перечислить тех, чей код вы разрешаете билдить, иначе на вашем сервере будут билдить все кому не лень, и даже хайджекнуть. Сам билдер очень простой: builder() -> receive {build,Repo,User} -> build(Repo,User) end, builder(). build(Repo,User) -> Docroot = "repos/" ++ Repo, Buildlogs = "buildlogs/"++ User ++ "-" ++ Repo, error_logger:info_msg("Hook worker called ~p",[Docroot]), {{Y,M,D},{H,Min,S}} = calendar:now_to_datetime(now()), LogFolder = io_lib:format("~p-~p-~p ~p:~p:~p",[Y,M,D,H,Min,S]), os:cmd(["mkdir -p \"",Docroot,"\""]), os:cmd(["mkdir -p \"",Buildlogs,"/",LogFolder,"\""]), Ctx = {User,Repo,Docroot,Buildlogs,LogFolder}, case os:cmd(["ls ",Docroot]) of [] -> os:cmd(["git clone git@github.com:",User,"/",Repo,".git",Docroot]); _ -> ok end, Script = ["git pull","rebar get-deps","rebar compile","./stop.sh","./release.sh", "./styles.sh","./javascript.sh","./start.sh"], [ cmd(Ctx,No,lists:nth(No,Script)) || No <- lists:seq(1,length(Script)) ]. Здесь в переменной Script -- то, что вы хотите выполнять. Ну сама команда cmd, которая пишет логи и т.д. cmd({User,Repo,Docroot,Buildlogs,LogFolder},No,List) -> Message = os:cmd(["cd ",Docroot," && ",List]), FileName = binary_to_list(base64:encode(lists:flatten([integer_to_list(No)," ",List]))), File = lists:flatten([Buildlogs,"/",LogFolder,"/",FileName]), error_logger:info_msg("Command: ~p",[List]), error_logger:info_msg("Output: ~p",[Message]), file:write_file(File,Message). Все, этого достаточно. Теперь можно написать REST интерфейс к этому серверу, на N2O это выглядит так: -module(releases_rest). -compile(export_all). -include("releases.hrl"). % PUBLIC REST INTERFACE FOR GLOBAL SERVER -define(RELS, [#release{id="5HT-skyline",name="5HT-skyline",user="5HT",repo="skyline"}] ). init() -> ets:new(releases, [named_table,{keypos,#release.id},public]), ets:insert(releases, ?RELS). get([]) -> ets:foldl(fun(C,Acc) -> [C|Acc] end,[],releases); get(Id) -> ets:lookup(releases,Id). delete(Id) -> ets:delete(releases,Id). put(R=#release{}) -> ets:insert(releases,R). exists(Id) -> ets:member(releases,Id). to_html(R=#release{}) -> [<<"">>,coalesce(R#release.id),<<">, <<"">>,coalesce(R#release.user),<<"">>, <<"">>,coalesce(R#release.repo),<<"">>, coalesce(R#release.name),<<"">>]. coalesce(Name) -> case Name of undefined -> <<>>; A -> list_to_binary(A) end. Ну и теперь напишем вебсайтик, который будет состоять из одной странцы: Вывод всех релизов: releases() -> Builds = string:tokens(os:cmd(["ls -1 buildlogs"]),"\n"), [ #h1{ body = "Continuos Integration"}, #h2{ body = "Builds" }, [ #p{ body = #link { body = R, url= "/index?release="++R }} || R <- Builds ] , #br{},#br{},#br{}, #span{ body = "© Synrc Research Center" } ]. Вывод билдов для каждого релиза: builds(Release) -> error_logger:info_msg("builds: ~p",[Release]), Builds = string:tokens(os:cmd(["ls -1 buildlogs/",Release]),"\n"), [ #h2{ body = "Builds for " ++ Release }, [ #p{ body = #link { body = B, url= "/index?release="++Release++"&build="++B }} || B <- Builds ] ]. Вывод шагов для каждого релиза: steps(Release,Build) -> error_logger:info_msg("steps: ~p ~p",[Release,Build]), Steps = string:tokens(os:cmd(["ls -1 \"buildlogs/",Release,"/",Build,"\""]),"\n"), [ #h2{ body = "Steps for " ++ Build ++ " build of " ++ Release ++ " release" }, [ #p{ body = #link { body = wf:to_list(base64:decode(S)), url= "/index?release="++Release++"&build="++Build++"&log="++S }} || S <- lists:sort(Steps) ] ]. Вывод лога: log(Release,Build,Step) -> error_logger:info_msg("log: ~p ~p ~p",[Release,Build,Step]), {ok,Bin} = file:read_file(["buildlogs/",Release,"/",Build,"/",Step]), [<<"">>,Bin,<<"">>]. Ну и сам модуль страницы: -module(public_index). -compile(export_all). -include_lib("n2o/include/wf.hrl"). -include_lib("kernel/include/file.hrl"). -include_lib("releaseman/include/releases.hrl"). main() -> [ #dtl{file = "index", app=releaseman,bindings=[{title,title()},{body,body()}]} ]. title() -> [ <<"RELEASE MANAGER">> ]. body() -> case {wf:qs(<<"release">>),wf:qs(<<"build">>),wf:qs(<<"log">>)} of {undefined,undefined,undefined} -> releases(); {Release,undefined,undefined} -> builds(binary_to_list(Release)); {Release,Build,undefined} -> steps(binary_to_list(Release),binary_to_list(Build)); {Release,Build,Step} -> log(binary_to_list(Release), binary_to_list(Build),binary_to_list(Step)) end. Вот как это выглядит, наш Билд сервер: http://build.synrc.com/